import React from "react";
import { HTML5Backend } from 'react-dnd-html5-backend'
import { DndProvider } from "react-dnd";
import { v4 as uuid } from "uuid";
import { uniqWith, throttle } from "lodash";
import BluePrintCanvas from "./butterfly";
import BaseEdge from "./butterfly/edge";
import BaseNode from "./butterfly/node";
import DragWrapper from "./components/DragWrapper";
import DropWrapper from "./components/DropWrapper";
import { generatorButterflyData } from "./utils";
import { buildInControl } from "./constants";
import 'butterfly-dag/dist/index.css';
import "./index.less";

const separator = "__$__";
interface IBlueprintProps {
  config: any;
  initGraph?: any;
  onChange?: (graph: any) => any;
}
interface IBlueprintState {
  graphState: any;
}
export default class BluePrint extends React.Component<IBlueprintProps, IBlueprintState> {
  canvas: any;
  canvasWrapper: any = React.createRef();
  constructor(props: IBlueprintProps) {
    super(props);
    this.state = {
      graphState: props.initGraph || {}
    }
  }
  componentDidMount() {
    this.initCanvas();
    window.addEventListener("keydown", this.keyboardDistribution)
  }
  componentWillUnmount() {
    const { onChange } = this.props;
    const { graphState } = this.state;
    /* 卸载页面之前做一次graphState的抛出，保证对于位置的变动能够同步给外面 */
    onChange && onChange(graphState);
    window.removeEventListener("keydown", this.keyboardDistribution);
  }
  initCanvas = () => {
    const { initGraph, config } = this.props;
    this.canvas = new BluePrintCanvas({
      root: this.canvasWrapper.current,
      disLinkable: true,
      linkable: true,
      draggable: true,
      zoomable: true,
      moveable: true,
      theme: {
        edge: {
          shapeType: "AdvancedBezier",
          arrow: true,
          Class: BaseEdge
        }
      }
    });
    this.canvas.draw(initGraph ? generatorButterflyData(initGraph, config) : [], () => {
      this.canvas.setGridMode(true, {
        isAdsorb: false,
        theme: {
          shapeType: 'circle',
          gap: 20,
          circleRadiu: 1.5,
          circleColor: 'rgba(255, 255, 255, 0.8)'
        }
      });
    });
    this._initCanvasEvent();
  }
  _initCanvasEvent = () => {
    this.canvas.on("system.link.connect", this.handleLinkPoint);
    this.canvas.on("node.change", this.handleNodeChange);
    this.canvas.on("system.node.move", this.throttleHandleNodeMove);
  }
  handleLinkPoint = (data) => {
    const { links } = data;
    const { graphState } = this.state;
    const { onChange } = this.props;
    const uniqNext = (arrVal, othVal) => { return arrVal.id === othVal.id && arrVal.actionName === othVal.actionName };
    links.forEach(link => {
      const sourceNode = link.sourceNode?.options;
      const linkOptions = link.options;
      const [sourceId, sourceEndpoint] = (linkOptions.sourceEndpoint || linkOptions.source)?.split(separator);
      const [targetId, targetEndpoint] = (linkOptions.targetEndpoint || linkOptions.target)?.split(separator);
      const nextData = { id: targetId, actionName: targetEndpoint, eventName: sourceEndpoint };
      const newGraphSource = graphState[sourceId];
      switch (sourceNode.type) {
        case "node":
          !newGraphSource[sourceEndpoint] && (newGraphSource[sourceEndpoint] = { next: [] });
          newGraphSource[sourceEndpoint].next = uniqWith(
            [].concat(newGraphSource[sourceEndpoint]?.next || [], [nextData]), uniqNext);
          break;
        case "condition":
          let nextKey = `${sourceEndpoint}Next`;
          newGraphSource[nextKey] = uniqWith(
            [].concat(newGraphSource[sourceEndpoint] || [], [nextData]), uniqNext);
          break;
        default:
          newGraphSource.next = uniqWith(
            [].concat(newGraphSource.next || [], [nextData]), uniqNext);
          break;
      }
    })
    onChange && onChange(graphState)
    this.setState({ graphState: { ...graphState } })
  }
  handleNodeChange = (data) => {
    const { options, value } = data;
    const { graphState } = this.state;
    const { onChange } = this.props;
    const targetNode = graphState?.[options.id];
    if (targetNode) {
      switch (options.type) {
        case "serial":
          targetNode.funcBody = value;
          break;
        case "timer":
          targetNode.delay = value;
      }
      onChange && onChange(graphState);
      this.setState({ graphState: { ...graphState } });
    }
  }
  handleNodeMove = ({ nodes }) => {
    const { graphState } = this.state;
    (nodes || []).forEach(node => {
      const currentState = graphState[node.id];
      currentState.__butterfly = {
        ...currentState.__butterfly,
        top: node.top,
        left: node.left
      }
    });
    this.setState({ graphState: { ...graphState } })
  }
  throttleHandleNodeMove = throttle(this.handleNodeMove, 100);
  getCanvasWrapperStyle = () => {
    return this.canvasWrapper.current?.getBoundingClientRect() || {};
  }
  addControlToCanvas = (data, monitor) => {
    const { onChange } = this.props;
    const { graphState } = this.state;
    const { top = 0, left = 0 } = this.getCanvasWrapperStyle();
    const currentOffset = monitor.getClientOffset();
    const newNode = {
      id: data.id || uuid(),
      top: currentOffset.y - top,
      left: currentOffset.x - left,
      title: data.name,
      action: data.action,
      event: data.event,
      type: data.type || "node",
      Class: BaseNode
    };
    if (!graphState?.[newNode.id]) {
      graphState[newNode.id] = {
        type: newNode.type,
        __butterfly: {
          top: newNode.top,
          left: newNode.left
        }
      };
      onChange && onChange(graphState);
      this.setState({ graphState: { ...graphState } })
    }
    this.canvas.addNode(newNode);
  }
  /* 全局键盘事件分发 */
  keyboardDistribution = (ev) => {
    switch (ev.code) {
      case "Delete":
        this.deleteCurrentNode();
        break;
    }
  }
  deleteCurrentNode = () => {
    if (this.canvas?.activeNode) {
      const activeNode = this.canvas.activeNode;
      switch (activeNode.__type) {
        case "node":
          this.fromCanvasRemoveNode(activeNode);
          break;
        case "edge":
          this.fromCanvasRemoveEdge(activeNode);
          break;
      }
    }
  }
  /** 移除node节点 */
  fromCanvasRemoveNode = (node) => {
    const { graphState } = this.state;
    const { onChange } = this.props;
    this.canvas.removeNode(node.id);
    delete graphState[node.id]
    /** 删除所有指向当前节点的next边 */
    Object.keys(graphState).forEach(key => {
      const currentNode = graphState[key];
      switch (currentNode.type) {
        case "node":
        case "condition":
          Object.keys(currentNode).forEach(childKey => {
            if (currentNode[childKey]?.next) {
              currentNode[childKey].next = currentNode[childKey].next?.filter(item => item.id !== node.id);
            }
          });
          break;
        default:
          currentNode.next = (currentNode.next || [])?.filter(item => item.id !== node.id);
      }
    })
    onChange && onChange(graphState);
    this.setState({ graphState: { ...graphState } })
  }
  /** 移除edge边 */
  fromCanvasRemoveEdge = (edge) => {
    const { graphState } = this.state;
    const { onChange } = this.props;
    const { sourceEndpoint, targetEndpoint } = edge.options;
    const [sourceId, sourceAction] = sourceEndpoint.split(separator);
    const [targetId, _] = targetEndpoint.split(separator);
    const { type: sourceType } = edge.sourceNode.options;
    switch (sourceType) {
      case "node":
        graphState[sourceId][sourceAction].next = (graphState[sourceId]?.[sourceAction]?.next || []).filter(item => item.id !== targetId);
        break;
      case "condition":
        let sourceKey = `${sourceAction}Next`;
        graphState[sourceId][sourceKey] = (graphState[sourceId]?.[sourceKey] || []).filter(item => item.id !== targetId);
        break;
      default:
        graphState[sourceId].next = (graphState[sourceId]?.next || []).filter(item => item.id !== targetId);
    }
    onChange && onChange(graphState);
    this.setState({ graphState: { ...graphState } });
    this.canvas.removeEdge(edge.id);
  }

  render() {
    const { config } = this.props;
    const { graphState } = this.state;
    return <DndProvider backend={HTML5Backend}>
      <div className="visbar-blueprint">
        <div className="blueprint-tool">
          <div className="outline-tree">
            <div className="control-list">
              <DragWrapper key="global" type="controls"
                itemData={{ id: "global", ...buildInControl.global}} canDrag={!graphState.global}>
                <p className="control-item">{ buildInControl.global.name }</p>
              </DragWrapper>
              {Object.keys(config).map(key => {
                const conf = config[key];
                return (<DragWrapper key={key} type="controls"
                  itemData={{ id: key, ...conf}} canDrag={!graphState[key]}>
                  <p className="control-item">{ conf.name }</p>
                </DragWrapper>)
              })}
            </div>
          </div>
          <div className="build-in-control">
            <div className="hander-list">
              <p className="title">内置处理器</p>
              {Object.keys(buildInControl.handler).map(key => {
                const currentControl = buildInControl.handler[key];
                return (<DragWrapper key={key} type="controls" itemData={{ key, ...currentControl }} canDrag={true}>
                  <p className="control-item">{ currentControl.name }</p>
                </DragWrapper>)
              })}
            </div>
            <div className="emitter-list">
              <p className="title">内置触发器</p>
              {Object.keys(buildInControl.emitter).map(key => {
                const currentControl = buildInControl.emitter[key];
                return (<DragWrapper key={key} type="controls" itemData={{ key, ...currentControl }} canDrag={true}>
                  <p className="control-item">{ currentControl.name }</p>
                </DragWrapper>)
              })}
            </div>
          </div>
        </div>
        <DropWrapper accept="controls" onDrop={this.addControlToCanvas}>
          <div className="blueprint-canvas" ref={this.canvasWrapper}></div>
        </DropWrapper>
      </div>
    </DndProvider>
  }
}